module DrunkenMaster
  class Repository
    class Persisted
      attr_accessor :revision, :object, :uuid, :deleted, :_info, :_data

      def state
        {'revision' => revision, 'uuid' => uuid, 'deleted' => deleted}
      end

      def state= s
        #p s
        self.revision = s['revision']
        self.uuid = s['uuid']
        self.deleted = s['deleted']
      end

      def delete
        self.deleted = true
        self
      end

      def object= o
        @object = o.tap {
          if o.respond_to? 'persisted='
            o.persisted = self
          end
        }
      end

      def deleted?
        self.deleted
      end

      def self.unserialize string
        (Persisted.new).tap { |p|
          p.unserialize(string)
        }
      end
      

      def serialize
        if object
          format, serialized = serialize_object(object)
        else
          format = _info['format']
          serialized = _data
        end
        return {:format => format, :info => state}.to_json + "\n" + serialized
      end

      def serialize_object object
        format = nil
        if object.respond_to? 'serialize'
          format = [:custom, object.class]
          serialized = custom_serialize object
        elsif object.respond_to? 'serialize_to_hash'
          format = [:serialized_to_hash, object.class]
          serialized = serialize_to_hash(object)
        else
          format = [:marshal, nil]
          serialized = Marshal::dump(object)
        end
        [format, serialized]
      end

      def custom_serialize object
        object.serialize.tap { |serialized|
          unless serialized
            raise "Serialize method for #{object.class} should not return nil or false (was #{serialized.inspect})."
          end
        }
      end

      def serialize_to_hash object
        object.serialize_to_hash.tap { |serialized|
          unless serialized.is_a? Hash
            raise "should be a hash, was #{serialized} (#{serialized.class})"
          end
        }.to_json
      end



      def unserialize string
        info, data = string.split("\n", 2)
        self._info = JSON.parse(info)
        begin
          # When the deserialization fails,
          # the persisted object would be completely useless.
          # Instead we catch the problem, log it and carry on.
          self.object = unserialize_object self._info, data
        rescue
          warn $!.inspect+"\n"+$!.backtrace.join("\n")
          self._data = data
        end
        self.state = self._info['info']
      end



      def object_name
        if self._info['format'].is_a? Array
          self._info['format'][1]
        else
          raise "this objects format is #{self._info['format']}"
        end
      end

      def object_name= new_name
        if self._info['format'].is_a? Array
          self._info['format'][1] = new_name
        else
          raise "this objects format is '#{self._info['format']}'."
        end
      end
      
      
    protected

      def unserialize_object info, data
        format = info['format']
        if format[0] == 'marshal'
          begin
            data = Marshal::load data
          rescue Exception => err
            raise err.message + " Data was '#{data}'."
          end
        elsif format[0] == 'custom'
          data = Magic.to_const(format[1]).unserialize(data)
        elsif format[0] == 'serialized_to_hash'
          data = Magic.to_const(format[1]).unserialize_hash(JSON.parse(data))
        else
          raise "unrecognized format '#{info['format'].inspect}'."
        end
        data
      end


    end
  end
end